﻿using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Timers;
using System.Xml.Linq;

namespace GalacticLib
{
 //   [assembly: AssemblyVersionAttribute("0.1.*")]
    public class DataRunProcessor
    {
        public delegate void OutputDelegate(string s);
        public OutputDelegate OutputEvent;
        public Uri ServerUri;
        public bool AutoProcess;
        public string ClientUniqueId;
       // List<MorphologyFilter> MorphologyFilters;
        List<DataSetData> Data;
        List<PDValue> PDValues;
        DataRunRequest ActiveRequest;
        DateTime StartedProcessing;
        DateTime FinishedProcessing;
        int LastDataSetId;

        Timer WorkerTimer;
        /// <summary>
        /// ms to wait after finishing a run to check for another
        /// </summary>
        double ActiveWorkerTimerInterval = 1000;
        /// <summary>
        /// If there are no pending runs use a longer interval
        /// </summary>
        double NoWorkWorkerTimerInterval = 1000 * 60;




     //   IGalacticClientUI Ui;


        public Version Version
        {
            get
            { 
                Assembly thisAssem = typeof(DataRunProcessor).Assembly;
                AssemblyName thisAssemName = thisAssem.GetName();

                return thisAssemName.Version;
            }
        }


        delegate void ReportProgressDelegate(long progress);

        public bool Processing { get; protected set; }
      

        public DataRunProcessor(string config)
        {
         //   Ui = ui;

            XDocument xd = XDocument.Parse(config);
            XElement Config = xd.Element("Config");

            ServerUri = new Uri(Config.Element("Uri").Value);
            AutoProcess = Config.Element("AutoProcess").Value.ToLower() == "true";
            ClientUniqueId = Config.Element("ClientUniqueId").Value;
           // MorphologyFilters = new List<MorphologyFilter>();
            Data = new List<DataSetData>();
            PDValues = new List<PDValue>(); 

          

            WorkerTimer = new Timer(ActiveWorkerTimerInterval);
            WorkerTimer.Elapsed += WorkerTimer_Elapsed;
            WorkerTimer.AutoReset = false;

            OutputEvent = new OutputDelegate(dummy);

            if (AutoProcess)
                Start();
        }

        void WorkerTimer_Elapsed(object sender, ElapsedEventArgs e)
        {
            Worker();
        }
        void dummy(string s)
        { 
        }
        void Output(string s)
        {
            OutputEvent.Invoke(s);
        }

        public static double ConvertToRadians(double degrees)
        {
            return Math.PI * degrees / 180.0;
        }

        public static double ConvertToDegrees(double radians)
        {
           return radians * (180.0 / Math.PI);
        }

        public void ServerRequestAsync(string method, object a)
        {

            try
            {
                var type = a.GetType();
                var props = type.GetProperties();
                var pairs = props.Select(x => x.Name + "=" + x.GetValue(a, null)).ToArray();
                var result = string.Join("&", pairs);

                HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(ServerUri);

                req.Method = "POST";
                req.Accept = "application/json, text/javascript, */*";
                req.Date = DateTime.Now;
                req.ContentType = "application/x-www-form-urlencoded";
           
                string postData = result;
                
                /// add sproc to request parameters
                if (postData != "")
                    postData += "&";
                postData += "sproc=" + method;

                ASCIIEncoding encoding = new ASCIIEncoding();
                byte[] formData = encoding.GetBytes(postData);

                req.ContentLength = formData.Length;
                Stream stream = req.GetRequestStream();
                stream.Write(formData, 0, formData.Length);
                stream.Close();

                AsyncRequestState state = new AsyncRequestState();
                state.Request = req;
                state.Started = DateTime.UtcNow;
            
                IAsyncResult r = (IAsyncResult) req.BeginGetResponse(new AsyncCallback(ServerRequestAsyncCallback), state);


            }
            catch (Exception e)
            {
                Output("ERROR: " + Environment.NewLine + e.Message + Environment.NewLine + e.StackTrace + Environment.NewLine + e.Source + Environment.NewLine + e.GetType().ToString());
                Stop();
                throw e;
            }
            finally
            {

            }
       
        }

        void ServerRequestAsyncCallback(IAsyncResult res)
        {
            AsyncRequestState state = (AsyncRequestState)res.AsyncState;
         
            HttpWebResponse response = (HttpWebResponse)state.Request.GetResponse();
            Stream responseStream = response.GetResponseStream();
            StreamReader reader = new StreamReader(responseStream);
            string responseFromServer = reader.ReadToEnd();

            reader.Close();
            responseStream.Close();
            response.Close();

            state.Finished = DateTime.UtcNow;
            Output("Async Request Completed in " + (state.Finished - state.Started) + " Response:" + responseFromServer);
                       
        }


        public string ServerRequest(string method, object a)
        {

            try
            {
                var type = a.GetType();
                var props = type.GetProperties();
                var pairs = props.Select(x => x.Name + "=" + x.GetValue(a, null)).ToArray();
                var result = string.Join("&", pairs);

                HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(ServerUri);

                req.Method = "POST";
                req.Accept = "application/json, text/javascript, */*";
                req.Date = DateTime.Now;
                req.ContentType = "application/x-www-form-urlencoded";
                //  req.KeepAlive = false;
                //  req.AllowWriteStreamBuffering = true;
                //   req.SendChunked = true;

                //build form data for request
                string postData = result;

                /*   foreach (KeyValuePair<string, object> parameter in parmaeters)
                    {
                        if (postData != "")
                            postData += "&";

                        postData += parameter.Key + "=" + parameter.Value;
                    }*/

                /// add sproc to request parameters
                if (postData != "")
                    postData += "&";
                postData += "sproc=" + method;


                ASCIIEncoding encoding = new ASCIIEncoding();
                byte[] formData = encoding.GetBytes(postData);

                req.ContentLength = formData.Length;
                Stream stream = req.GetRequestStream();
                stream.Write(formData, 0, formData.Length);
                stream.Close();

                HttpWebResponse response = (HttpWebResponse)req.GetResponse();
                Stream responseStream = response.GetResponseStream();
                StreamReader reader = new StreamReader(responseStream);
                string responseFromServer = reader.ReadToEnd();

                reader.Close();
                responseStream.Close();
                response.Close();

                return responseFromServer;
            }
            catch(Exception e)
            {
                Output("ERROR: " + Environment.NewLine + e.Message + Environment.NewLine + e.StackTrace + Environment.NewLine + e.Source + Environment.NewLine + e.GetType().ToString());
                Stop();
                throw e;
            }
            finally
            {
              
            }
           // return "";
        }

        public void Start()
        {
            ServerRequest("usp_RegisterClient",
                       new
                       {
                           Hostname = Environment.MachineName,
                           Version = "",
                           GalacticClientVersion = this.Version,
                           OS = Environment.OSVersion.ToString()
                       }
                       );

            Processing = true;
            WorkerTimer.Start();
            Output("Starting");
        }

        public void Stop()
        {
            Output("Stopping...");
            if (ActiveRequest != null)
            {
                ServerRequest("usp_ReturnDataRun",
                         new
                         {
                             DataRunRequestId = ActiveRequest.DataRunRequestId
                         }
                         );
            }

            Processing = false;
            WorkerTimer.Stop();
            Output("Stopped.");
        }

        void Worker()
        {
            if (Processing)
            {
                Output("Worker()");
                
                DataRunRequest req = CheckForDataRun();

                if (req != null)
                {
                    ProcessDataRunRequest(req);
                    WorkerTimer.Interval = ActiveWorkerTimerInterval;
                }
                else
                {
                    Output("Signalling to UI there are no pending runs");
                  //  Ui.NoPendingRuns();
                    Output("No pending data runs! Using longer timer interval");
                    WorkerTimer.Interval = NoWorkWorkerTimerInterval;
                    
                }
            }

            WorkerTimer.Start();
            
           
        }

        public string GetConfigString()
        {
            XDocument xd = new XDocument(new XDeclaration("1.0", "utf-8", null));
            XElement config = new XElement("Config");

            XElement uri = new XElement("Uri", ServerUri);
            XElement autoprocess = new XElement("AutoProcess", AutoProcess);
            XElement clientUniqueId = new XElement("ClientUniqueId", ClientUniqueId);

            config.Add(uri);
            config.Add(autoprocess);
            config.Add(clientUniqueId);
            xd.Add(config);
            return xd.Declaration + Environment.NewLine + xd.ToString();
        }

        public static List<DataSetData> GetSecondaries(DataSetData prim, List<DataSetData> data, DataRunRequest req)
        {
             //get secondaries
                List<DataSetData> subset = data.AsParallel().Where(sec =>

                    Regex.IsMatch(sec.Morphology, req.SecondaryRegex, RegexOptions.IgnoreCase)

                    &&

                   ((!req.Is3D) ? (GreatCircle(prim, sec) <= req.FittingRangeRad) : (GreatCircle(prim, sec) <= req.FittingRange / prim.Distance))
                   //vbin
                   && ((req.Is3D && req.vmin != null && req.vmax != null) ? req.vmin <= sec.v && sec.v <= req.vmax : true)


                   && sec != prim

                    ).ToList();

            return subset;
        }

        public void ProcessDataRunRequest(DataRunRequest req)
        {
            StartedProcessing = DateTime.UtcNow;
            ActiveRequest = req;
            PDValues.Clear();
          //  Ui.StartingRun(req);

            if (LastDataSetId != ActiveRequest.DataSetId)
            {
                LastDataSetId = ActiveRequest.DataSetId;
            //    DownloadMorphologyFilters();
                DownloadDataSet();
            }
            //DownloadDataSetLocal(); //for dev without a running instance of server
          
            string deets = JsonConvert.SerializeObject(req, Formatting.Indented);
            Output(deets);         
            
          //  req.PrimaryRegex = MorphologyFilters.First(f => f.Name == req.PrimaryMorph).RegexFilter;
          //  req.SecondaryRegex = MorphologyFilters.First(f => f.Name == req.SecondaryMorph).RegexFilter;
           
            // primary morphology filters
            //todo pull from data request.                           
            //remove objects too near latitude border
            double latborder = ConvertToRadians(ActiveRequest.LatBoundDegree);
            
            List<DataSetData> primaries = Data.AsParallel().Where(             
                p =>
                    ((!ActiveRequest.Is3D) ?
                    GreatCircle(p.glatrad, p.glonrad, latborder, p.glonrad) >= ActiveRequest.FittingRangeRad // remove too near to border
                    			//and dbo.udf_GetDistanceRad(data.glonrad, data.glatrad, data.glonrad, @latbound) * data.Distance >= @minbound -- projected 3d minbound			
                  : GreatCircle(p.glatrad, p.glonrad, latborder, p.glonrad) * p.Distance > ActiveRequest.FittingRange // remove too near to border
                    )

                    //vbin
                    &&
                    ((ActiveRequest.Is3D && ActiveRequest.vmin != null && ActiveRequest.vmax != null) ? ActiveRequest.vmin <= p.v && p.v <= ActiveRequest.vmax : true)

                  
                    &&
                    
                    Regex.IsMatch(p.Morphology, ActiveRequest.PrimaryRegex, RegexOptions.IgnoreCase) // remove incorrect primary morphologies

                //    && p.id == 143044

                ).ToList();

            long PrimariesProcessed = 0;
            int targetReportIncrement = 100;

            
            ServerRequest("usp_UpdatePrimaryCount",
                new
                {
                    DataRunRequestId = ActiveRequest.DataRunRequestId,
                    PrimaryCount = primaries.Count
                }
                );

            ReportProgressDelegate reportProgressDelegate = new ReportProgressDelegate(ReportProgress);
            List<PDValue> sendBatch = new List<PDValue>();

            primaries.ForEach( prim =>
            {
               if(!Processing)
                {
                      return;
                }

                PDValue pd = new PDValue();
                pd.SentToServer = false;

                List<DataSetData> subset = GetSecondaries(prim, Data, ActiveRequest);

               
                if (subset.Count >= 3)
                {
                    pd.DataId = prim.id;
                    pd.DataRunRequestId = ActiveRequest.DataRunRequestId;
                    pd.PD = GetPointwiseDimension(prim, subset);
                    pd.SecondaryCount = subset.Count;
                    // 3d NGC_3115 10mpc  pd=1.77
                
                    PDValues.Add(pd);
                    sendBatch.Add(pd);
                    
                }
               
                // Output(PrimariesProcessed + " / " + primaries.Count + " " + prim.GalaxyName + " d=" + pd.PD);
                if (++PrimariesProcessed % targetReportIncrement == 0)
                {
                    Output(PrimariesProcessed + " / " + primaries.Count + " in " + (DateTime.UtcNow - StartedProcessing));

                    IAsyncResult res = reportProgressDelegate.BeginInvoke(PrimariesProcessed, null, null);

                    ReportProgress(PrimariesProcessed);

                    List<PDValue> NotSentToServer = sendBatch.Where(p => p.SentToServer == false).ToList();
                    sendBatch.Clear();
                    NotSentToServer.ForEach(p => p.SentToServer = true);


                    SendDataToServerAsync(NotSentToServer);

                }
               
                
            });


            if (Processing)
            {
                List<PDValue> NotSentToServer = sendBatch.Where(p => p.SentToServer == false).ToList();
                sendBatch.Clear();
                NotSentToServer.ForEach(p => p.SentToServer = true);
                SendDataToServerAsync(NotSentToServer);
                ReportProgress(PrimariesProcessed);

                FinishedProcessing = DateTime.UtcNow;
             
               Output("Finished Processing in " + (FinishedProcessing - StartedProcessing));
            }

            ActiveRequest = null;
        }

        private void SendDataToServerAsync(List<PDValue> NotSentToServer)
        {
            /////////
            string serializedValues = JsonConvert.SerializeObject(NotSentToServer, Formatting.None);

            Output("Uploading Data for " + ActiveRequest.DataRunRequestId);
            ServerRequestAsync("SavePDRun",
                    new
                    {
                        results = serializedValues
                    });
                    
        }

        private void DownloadDataSetLocal()
        {
            string json = File.ReadAllText("2MRS North JSON.js");
            Data = JsonConvert.DeserializeObject<List<DataSetData>>(json);

        }


        public DataRunRequest CheckForDataRun()
        {

            string json;
            if (ClientUniqueId == "")
            {
                 json = ServerRequest("usp_CheckoutDataRun",
                    new
                    {
                        ClientId = Environment.MachineName
           
                    });
            }
            else
            {
                json = ServerRequest("usp_CheckoutDataRun",
                 new
                 {
                     ClientId = Environment.MachineName,
                     ClientUniqueId = ClientUniqueId
                 });
            }

            // will only be one but is presented as an array
            List<DataRunRequest> reqs = JsonConvert.DeserializeObject<List<DataRunRequest>>(json); 
            if (reqs.Count > 0)
            {
                DataRunRequest req = reqs.First();
                return req;
            }
            else
                return null;
       }

        void ReportProgress(long processed)
        {
            ServerRequest("usp_ReportProgress",
                new  
                { 
                    DataRunRequestId = ActiveRequest.DataRunRequestId,
                    ProcessedObjects = processed
                }
                );
        }


        public static List<DataSetData> OrderAndCalculatePDLogs(DataSetData primary, List<DataSetData> secondaries, DataRunRequest req)
        {
             if (!req.Is3D)
                secondaries.ForEach(sec => sec.PD_DistanceRad = GreatCircle(primary, sec)); //optimize by calculating earlier before distance comparison?
            else
                secondaries.ForEach(sec => sec.PD_DistanceRad = primary.Distance * GreatCircle(primary, sec)); //optimize by calculating earlier before distance comparison?


            List<DataSetData> orderedSecondaries = secondaries.OrderBy(sec => sec.PD_DistanceRad).ToList();

            ulong count = 0;

            orderedSecondaries.ForEach(sec =>
            {
                sec.PD_LogCount = Math.Log10(++count);
                if (!req.Is3D)
                    sec.PD_LogDistance = Math.Log10(ConvertToDegrees(sec.PD_DistanceRad));
                else
                    sec.PD_LogDistance = Math.Log10(sec.PD_DistanceRad);
            }
            );

            return orderedSecondaries;
        }

        public static double GetPointwiseDimension(DataSetData primary, List<DataSetData> secondaries, DataRunRequest req)
        {

            List<DataSetData> orderedSecondaries = OrderAndCalculatePDLogs(primary, secondaries, req);

            //double avgx, avgy, 
            double sumx, sumy, n, sumxsquares, sumxy;

            //avgx = orderedSecondaries.Average(sec => sec.PD_LogDistance);
            //avgy = orderedSecondaries.Average(sec => sec.PD_LogCount);
            sumx = orderedSecondaries.Sum(sec => sec.PD_LogDistance);
            sumy = orderedSecondaries.Sum(sec => sec.PD_LogCount);
            n = orderedSecondaries.Count;
            sumxsquares = orderedSecondaries.Sum(sec => sec.PD_LogDistance * sec.PD_LogDistance);
            sumxy = orderedSecondaries.Sum(sec => sec.PD_LogDistance * sec.PD_LogCount);

            double denominator = (sumxsquares - ((sumx * sumx) / n));
            if (denominator == 0.0)
                return 0.0;

            double d = 0;

            d = (sumxy - ((sumx * sumy) / n)) / denominator;

            return d;
        }
        double GetPointwiseDimension(DataSetData primary, List<DataSetData> secondaries)
        {
            return GetPointwiseDimension(primary, secondaries, ActiveRequest);
        }

        public static double GreatCircle(DataSetData primary, DataSetData secondary)
        {
           return GreatCircle(primary.glatrad, primary.glonrad, secondary.glatrad, secondary.glonrad);
        }

        public static double GreatCircle(double lat1, double lon1, double lat2, double lon2)
        {

            double distrad = 
                Math.Cos( Math.Abs(lon1 - lon2) ) 
                * Math.Cos(lat1) 
                * Math.Cos(lat2) 
                + 
               Math.Sin(lat1) * Math.Sin(lat2);

            distrad = Math.Acos(distrad);
            return distrad;
        }

        void DownloadDataSet()
        {                      
            Output("Download Data Set");
             string response = ServerRequest("usp_GetDataSetData",
                new
                {
                    id = ActiveRequest.DataSetId
                }
                );
                        
            Data = JsonConvert.DeserializeObject<List<DataSetData>>(response);
                   
        }

    /*    void DownloadMorphologyFilters()
        {
            string response = ServerRequest("usp_GetMorphologyFilters",
                new
                {
                    DataSetId = ActiveRequest.DataSetId
                }
                );
                        
            MorphologyFilters = JsonConvert.DeserializeObject<List<MorphologyFilter>>(response);
        }*/

    }
}
